#!/usr/bin/env python2

# Copyright (c) Air Ins 2011
# Author: Benjamin Vanheuverzwijn & Marco Caron

import sys
import re
import serial
import Queue
import datetime
import random
import csv
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4 import Qt
import PyQt4.Qwt5 as Qwt

#
# Manometer application
#
class Manometer_Application:
    def run(self):
        app = QtGui.QApplication(sys.argv)

        ui = Manometer_MainWindow()
        ui.show()

        return app.exec_()

#
# Application main window
#
class Manometer_MainWindow(QtGui.QMainWindow):
    def __init__(self, win_parent=None):
        QtGui.QMainWindow.__init__(self, win_parent)

        self.central_widget = QtGui.QWidget()
        self.captures_layout = QtGui.QHBoxLayout()

        self.central_widget.setLayout(self.captures_layout)
        self.setCentralWidget(self.central_widget)
        self.setWindowTitle("Manometer application")
        self.resize(640, 480)

        self.start_menu = None
        self.stop_menu = None

        self.createMenuBar()
        self.createStatusBar()


    # Create the menu bar
    def createMenuBar(self):
        # File
        file_menu = self.menuBar().addMenu("File")

        # File - Quit
        quit_action = QtGui.QAction("Quit", QtGui.qApp)
        quit_action.setShortcut('ctrl+q')
        self.connect(quit_action, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
        file_menu.addAction(quit_action)

        # Capture
        capture_menu = self.menuBar().addMenu("Capture")

        # Capture - New capture
        newcapture_action = QtGui.QAction("Add capture", QtGui.qApp)
        newcapture_action.setShortcut('ctrl+a')
        self.connect(newcapture_action, QtCore.SIGNAL('triggered()'), self.addCaptureAction)
        capture_menu.addAction(newcapture_action)

        # Help
        help_menu = self.menuBar().addMenu("Help")

        # Help - About
        about_action = QtGui.QAction("About", QtGui.qApp)
        self.connect(about_action, QtCore.SIGNAL('triggered()'), self.aboutAction)
        help_menu.addAction(about_action)

    # Create the status bar
    def createStatusBar(self):
        self.statusBar().showMessage("Ready")

    # AddCaptureAction handler
    def addCaptureAction(self):
        # show new capture dialog
        addcapture = Manometer_AddCaptureDialog()
        code = addcapture.exec_()

        if code == QtGui.QDialog.Accepted:
            livecapture_layout = QtGui.QVBoxLayout()

            livecapturestatusbar = Manometer_LiveCaptureStatusbar()
            livecaptureform = Manometer_LiveCaptureForm()

            livecaptureplot = Manometer_LiveCapturePlot(livecaptureform, livecapturestatusbar, addcapture.serial_port, addcapture.serial_baudrate, addcapture.capture_beginin, addcapture.capture_duration, addcapture.export_type, addcapture.export_filepath)

            livecapture_layout.addWidget(Manometer_LiveCaptureToolbar(livecaptureplot))
            livecapture_layout.addWidget(livecaptureplot)
            livecapture_layout.addWidget(livecaptureform)
            livecapture_layout.addWidget(livecapturestatusbar)

            self.captures_layout.addLayout(livecapture_layout)
            livecapturestatusbar.showMessage("Added a new capture ready to start in " + str(addcapture.capture_beginin.total_seconds()) + " sec for " + str(addcapture.capture_duration.total_seconds()) + " sec")
        else:
            self.statusBar().showMessage("New capture cancelled")

    # AboutAction handler
    def aboutAction(self):
        QtGui.QMessageBox.about(self, "About", "Copyright (c) Air Ins 2011\nAuthor: Benjamin Vanheuverzwijn")

#
# Live capture status bar
#
class Manometer_LiveCaptureStatusbar(QtGui.QStatusBar):
    def __init__(self, parent = None):
        # parent constructor
        QtGui.QStatusBar.__init__(self, parent)

        # disable the resize
        self.setSizeGripEnabled(False)

class Manometer_LiveCaptureForm(QtGui.QWidget):
    def __init__(self, parent = None):
        QtGui.QWidget.__init__(self)

        main_layout = QtGui.QHBoxLayout()

        #
        # Form layout
        #
        form_layout = QtGui.QFormLayout()
        main_layout.addLayout(form_layout)

        # X scale
        self.xscale_input = QtGui.QLineEdit()
        form_layout.addRow("Plot period (s)", self.xscale_input)

        #
        # Display layout
        #
        display_layout = QtGui.QVBoxLayout()
        main_layout.addLayout(display_layout)

        # Channel A label
        self.channela_label = QtGui.QLabel()
        self.channela_label.setText("Channel A: 0.0 Pa")
        display_layout.addWidget(self.channela_label)

        self.channelb_label = QtGui.QLabel()
        self.channelb_label.setText("Channel B: 0.0 Pa")
        display_layout.addWidget(self.channelb_label)

        # Main layout
        self.setLayout(main_layout)

    def setChannelValues(self, channel_a, channel_b):
        self.channela_label.setText("Channel A: " + str(channel_a) + " Pa")
        self.channelb_label.setText("Channel B: " + str(channel_b) + " Pa")

    def getXScaleValue(self):
        value = str(self.xscale_input.text())
        if not value:
            value = "0"
        return value

#
# Live capture toolbar
#
class Manometer_LiveCaptureToolbar(QtGui.QToolBar):
    # Constructor
    def __init__(self, capture):
        # parent
        QtGui.QToolBar.__init__(self)

        self.capture = capture

        # Capture - Start
        self.start_action = QtGui.QAction("Start", QtGui.qApp)
        self.start_action.setEnabled(True)
        self.connect(self.start_action, QtCore.SIGNAL('triggered()'), self.startCapture)
        self.addAction(self.start_action)

        # Capture - Stop
        self.stop_action = QtGui.QAction("Stop", QtGui.qApp)
        self.stop_action.setEnabled(False)
        self.connect(self.stop_action, QtCore.SIGNAL('triggered()'), self.stopCapture)
        self.addAction(self.stop_action)

        # Capture - Reset
        self.reset_action = QtGui.QAction("Reset", QtGui.qApp)
        self.reset_action.setEnabled(False)
        self.connect(self.reset_action, QtCore.SIGNAL('triggered()'), self.resetCapture)
        self.addAction(self.reset_action)

        self.addSeparator()

        # Capture - Remove
        self.remove_action = QtGui.QAction("Remove", QtGui.qApp)
        self.remove_action.setEnabled(False)
        self.addAction(self.remove_action)


    # Start a capture
    def startCapture(self):
        self.capture.startCapture()
        self.start_action.setEnabled(False)
        self.stop_action.setEnabled(True)

    # Stop a capture
    def stopCapture(self):
        self.capture.stopCapture()
        self.start_action.setEnabled(True)
        self.stop_action.setEnabled(False)

    # Reset a capture
    def resetCapture(self):
        self.stopCapture()
        self.capture.reset()

    
#
# Widget that display a current capture
#
class Manometer_LiveCapturePlot(QtGui.QWidget):
    # Constructor
    # serial_port: serial port name
    # serial_baudrate: serial port baudrate
    # capture_beginin: begin the capture in X seconds
    # capture_duration: end the capture after Y seconds
    # export_type: export the data in this format (csv, excel, ...)
    # export_filepath: export the data in this file
    def __init__(self, captureform, statusbar, serial_port, serial_baudrate, capture_beginin, capture_duration, export_type, export_filepath):
        # parent constructor
        QtGui.QWidget.__init__(self)

        self.createPlot()

        plot_layout = QtGui.QVBoxLayout()
        plot_layout.addWidget(self.plot)

        self.setLayout(plot_layout)

        self.data = []

        self.read_queue = Queue.Queue()

        if serial_port == "fake":
            self.serial_thread = Manometer_SerialThreadMock(serial_port, serial_baudrate, self.read_queue)
        else:
            self.serial_thread = Manometer_SerialThread(serial_port, serial_baudrate, self.read_queue)

        self.start_capture_timer = None
        self.stop_capture_timer = None
        self.read_timer = None

        # If we start the manometer *before* reading the manometer, it will have
        # have a time offset in each packet
        self.time_offset = 0

        if capture_beginin.total_seconds() > 0:
            self.startCapture(capture_beginin.total_seconds(), capture_duration.total_seconds())

        self.export_filepath = export_filepath
        self.csv_writer = None
        self.fd = None

        self.captureform = captureform
        self.statusbar = statusbar

    # Create the basic plot
    def createPlot(self):
        plot = Qwt.QwtPlot(self)
        zoomer = Qwt.QwtPlotZoomer(plot.canvas())
        zoomer.initMousePattern(3)

        # Legend
        legend = Qwt.QwtLegend()
        plot.insertLegend(legend, Qwt.QwtPlot.RightLegend)

        # X axis
        plot.setAxisTitle(Qwt.QwtPlot.xBottom, 'Time (s)')
        plot.setAxisScale(Qwt.QwtPlot.xBottom, 0, 10, 1)

        # Y axis
        plot.setAxisTitle(Qwt.QwtPlot.yLeft, 'Pressure (Pa)')
        plot.setAxisScale(Qwt.QwtPlot.yLeft, 0, 100, 10)

        plot.replot()

        # Channel A curve
        curve_channela = Qwt.QwtPlotCurve('Channel A')
        curve_channela.setRenderHint(Qwt.QwtPlotItem.RenderAntialiased)
        pen = QtGui.QPen(QtGui.QColor('red'))
        pen.setWidth(2)
        curve_channela.setPen(pen)
        curve_channela.attach(plot)

        # Channel B curve
        curve_channelb = Qwt.QwtPlotCurve('Channel B')
        curve_channelb.setRenderHint(Qwt.QwtPlotItem.RenderAntialiased)
        pen = QtGui.QPen(QtGui.QColor('blue'))
        pen.setWidth(2)
        curve_channelb.setPen(pen)
        curve_channelb.attach(plot)

        self.plot = plot
        self.curve_channela = curve_channela
        self.curve_channelb = curve_channelb
        self.zoomer = zoomer

    def startCapture(self, delay=0, duration=0):
        if delay > 0:
            if not self.start_capture_timer:
                self.start_capture_timer = QtCore.QTimer()
                self.connect(self.start_capture_timer, QtCore.SIGNAL('timeout()'), self.startCapture)

            self.start_capture_timer.start(delay * 1000)

            # timer to stop the capture
            if duration > 0:
                self.stop_capture_timer = QtCore.QTimer()
                self.stop_capture_timer.connect(self.stop_capture_timer, QtCore.SIGNAL('timeout()'), self.stopCapture)
                self.stop_capture_timer.start((delay + duration) * 1000)
        else:
            if self.start_capture_timer:
                self.start_capture_timer.stop()
                del self.start_capture_timer


            # start to read the serial port
            self.serial_thread.start()

            # timer to read the data in the queue
            self.read_timer = QtCore.QTimer()
            self.connect(self.read_timer, QtCore.SIGNAL('timeout()'), self.onTimerCallback)
            self.read_timer.start(500) # 500 ms

            # csv
            if self.export_filepath:
                self.fd = open(self.export_filepath, "wb")

    def stopCapture(self):
        self.read_timer.stop()
        self.serial_thread.stop()
        if self.fd:
            self.fd.close()

        if self.stop_capture_timer:
            self.stop_capture_timer.stop()
            del self.stop_capture_timer

    def resetCapture(self):
        self.stopCapture()
        self.data = []

        plot.setAxisScale(Qwt.QwtPlot.xBottom, 0, 10, 1)
        plot.setAxisScale(Qwt.QwtPlot.yLeft, 0, 100, 5)

        self.curve_channela.setData((), ())
        self.curve_channelb.setData((), ())
        self.plot.replot()

    # Update plot on timer callback
    def onTimerCallback(self):
        try:
            pressure = self.read_queue.get(timeout=0.001)

            channel_a, channel_b, time_elapsed, flow, mode = pressure.split(",")

            channel_a = float(channel_a)
            channel_b = float(channel_b)
            time_elapsed = int(time_elapsed)

            print("Read:" + str(channel_a/4) + " Pa @ " + str(time_elapsed) + " sec")

            if self.time_offset == 0:
                self.time_offset = time_elapsed
            
            self.data.append((time_elapsed - self.time_offset, channel_a/4, channel_b/4))

            xdata = [s[0] for s in self.data]
            ydata_channela = [s[1] for s in self.data]
            ydata_channelb = [s[2] for s in self.data]
            
            minx = abs(int(self.captureform.getXScaleValue()))
            if minx < 0 or len(xdata) < minx:
                minx = 0
            else:
                minx = xdata[-minx]

            self.plot.setAxisScale(Qwt.QwtPlot.xBottom, minx, max(5, xdata[-1]))
            self.plot.setAxisScale(Qwt.QwtPlot.yLeft, min((min(ydata_channela),min(ydata_channelb))), max((max(ydata_channela),max(ydata_channelb))))

            self.curve_channela.setData(xdata, ydata_channela)
            self.curve_channelb.setData(xdata, ydata_channelb)
            self.plot.replot()

            if self.fd:
                if self.csv_writer is None:
                    self.csv_writer = csv.writer(self.fd)
                self.csv_writer.writerow([channel_a/4, channel_b/4, (time_elapsed - self.time_offset)])

            self.captureform.setChannelValues(channel_a/4, channel_b/4)

        except Queue.Empty:
            # Do nothing
            pass
#
# Start a new capture dialog
#
class Manometer_AddCaptureDialog(QtGui.QDialog):
    def __init__(self):
        # parent constructor
        QtGui.QWidget.__init__(self)

        # Main vertical layout
        main_layout = QtGui.QVBoxLayout()

        # Serial device model
        device_groupbox = QtGui.QGroupBox()
        device_groupbox.setTitle("Device options")

        device_layout = QtGui.QFormLayout()

        self.model_combobox = QtGui.QComboBox()
        self.model_combobox.addItem("Manometer XYZ")
        device_layout.addRow("Model", self.model_combobox)

        self.channel_combobox = QtGui.QComboBox()
        self.channel_combobox.addItem("Both")
        self.channel_combobox.addItem("Channel A")
        self.channel_combobox.addItem("Channel B")
        device_layout.addRow("Channels", self.channel_combobox)

        device_groupbox.setLayout(device_layout)
        main_layout.addWidget(device_groupbox)

        # Serial port
        input_groupbox = QtGui.QGroupBox()
        input_groupbox.setTitle("Serial port")

        input_layout = QtGui.QFormLayout()

        self.port_input = QtGui.QLineEdit()
        self.port_input.setText("COM0")

        self.baudrate_input = QtGui.QComboBox()
        self.baudrate_input.addItem("9600")

        input_layout.addRow("Port", self.port_input)
        input_layout.addRow("Baudrate", self.baudrate_input)

        input_groupbox.setLayout(input_layout)
        main_layout.addWidget(input_groupbox)

        # Capture delay / duration
        delay_groupbox = QtGui.QGroupBox()
        delay_groupbox.setTitle("Delay options")

        delay_layout = QtGui.QFormLayout()

        self.beginin_input = QtGui.QLineEdit()
        self.duration_input = QtGui.QLineEdit()

        delay_layout.addRow("Begin in", self.beginin_input)
        delay_layout.addRow("Duration", self.duration_input)
        
        delay_groupbox.setLayout(delay_layout)
        main_layout.addWidget(delay_groupbox)

        # Export file
        export_groupbox = QtGui.QGroupBox()
        export_groupbox.setTitle("Export options")

        export_layout = QtGui.QFormLayout()

        self.filepath_input = QtGui.QLineEdit()

        self.type_input = QtGui.QComboBox()
        self.type_input.addItem("CSV")
        self.type_input.addItem("Excel")

        export_layout.addRow("File path", self.filepath_input)
        export_layout.addRow("Type", self.type_input)

        export_groupbox.setLayout(export_layout)
        main_layout.addWidget(export_groupbox)

        # Buttons
        button_layout = QtGui.QHBoxLayout()

        cancel_button = QtGui.QPushButton("Cancel")
        self.connect(cancel_button, QtCore.SIGNAL('clicked()'), self.reject)
        button_layout.addWidget(cancel_button)

        launch_button = QtGui.QPushButton("Add")
        launch_button.setDefault(True)
        self.connect(launch_button, QtCore.SIGNAL('clicked()'), self.accept)
        button_layout.addWidget(launch_button)

        main_layout.addLayout(button_layout)

        # Set main layout to the dialog
        self.setLayout(main_layout)

        self.connect(self, QtCore.SIGNAL('accepted()'), self.onAccepted)

        # default value
        self.serial_port = None
        self.serial_baudrate = 9600
        self.capture_beginin = None
        self.capture_duration = None
        self.export_filepath = None
        self.export_type = None

    def onAccepted(self):
        tp = Manometer_DateParser()

        self.serial_port = str(self.port_input.text())
        self.serial_baudrate = str(self.baudrate_input.currentText())

        self.capture_beginin = tp.parse(str(self.beginin_input.text()))
        self.capture_duration = tp.parse(str(self.duration_input.text()))

        self.export_filepath = str(self.filepath_input.text())
        self.export_type = str(self.type_input.currentText())

#
# Serial port thread mocked
#
class Manometer_SerialThreadMock(QtCore.QThread):
    def __init__(self, port, baudrate, read_queue = None, parent = None):
        # parent constructor
        QtCore.QThread.__init__(self, parent)

        self.alive = 0
        self.read_queue = read_queue

        self.fake_data = Queue.Queue()
        for i in range(0, 60):
            rnd_channel_a = random.randint(0, i*4)
            rnd_channel_b = random.randint(0, i*4)
            time_elapsed = i
            self.fake_data.put("+" + str(rnd_channel_a) + ",+" + str(rnd_channel_b) + "," + str(time_elapsed) + ",0,1234\n")

    # run the thread
    def run(self):
        self.alive = 1

        while self.alive:
            data = self.fake_data.get()
            self.read_queue.put(data)
            self.msleep(1000)

    # stop the thread
    def stop(self):
        self.alive = 0

        # flush the read queue
        while not self.read_queue.empty():
            self.read_queue.get()

#
# Serial port thread
#
class Manometer_SerialThread(QtCore.QThread):
    def __init__(self, port, baudrate, read_queue = None, parent = None):
        # parent constructor
        QtCore.QThread.__init__(self, parent)

        self.alive = 0
        self.serial = serial.Serial(port, baudrate, timeout=0.01) # timeout: 10 ms
        self.read_queue = read_queue

    # run the thread
    def run(self):
        self.alive = 1

        while self.alive:
            data = self.serial.readline()

            if data:
                self.read_queue.put(data)

            # discard \r
            self.serial.read(1)

            self.msleep(100)

    # stop the thread
    def stop(self):
        self.alive = 0

        # flush data in the serial port
        self.serial.flushInput()
        # flush the read queue
        while not self.read_queue.empty():
            self.read_queue.get()

#
# Do some things with a date
#
class Manometer_DateParser():
    # Parse a time diff like "1 h 2 m", etc
    # and return the total seconds
    def parse(self, strdate):
        hours = 0
        minutes = 0
        seconds = 0
        result = re.search("((?P<hour>[0-9]+)\s?(h|heure|heures)\s?)?((?P<minute>[0-9]+)\s?(m|min|minute|minutes)\s?)?((?P<second>[0-9]+)\s?(s|sec|second|seconde|secondes))?", strdate)
        if result:
            if result.group('hour'):
                hours = int(result.group('hour'))
            if result.group('minute'):
                minutes = int(result.group('minute'))
            if result.group('second'):
                seconds = int(result.group('second'))
        return datetime.timedelta(seconds=seconds, minutes=minutes, hours=hours)
    
#
# main
#
if __name__ == "__main__":
    application = Manometer_Application();

    sys.exit(application.run())
